package quarto.controller.client;

import quarto.controller.Game;
import quarto.controller.Command;
import quarto.controller.Protocol;
import quarto.controller.server.Server;

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.net.Socket;
import java.util.ArrayList;
import quarto.Message;

/**
 * Een ClientHandler is de verbinding tussen een Client en de Server (in ons
 * geval is de Server het Game-Object). In de ClientHandler wordt een 
 * @author Frank Borgonjen & Tim Smit
 */
public class ClientHandler extends Thread implements Protocol
{
    /** @invariant game != null */
    private Game game;
    
    /** de client socket */
    private Socket sock;
    
    /** de server */
    private Server server;
    
    /** de invoerstream */
    private BufferedReader in;
    
    /** de uitvoerstream */
    private BufferedWriter out;
    
    /** de naam van de client */
    private String clientName = null;
    
    /** kan de speler chatten */
    private boolean canChat = false;

    /**
     * Construeert een ClientHandler en initialiseert beide data streams
     * @require sock != null
     */
    public ClientHandler(Socket sock, Server server) throws IOException
    {
        this.sock   = sock;
        this.server = server;
        
        // probeer de in- en output stream te initialiseren
        try
        {
            this.in = new BufferedReader(new InputStreamReader(sock.getInputStream()));
            this.out = new BufferedWriter(new OutputStreamWriter(sock.getOutputStream()));
        }
        // initialiseren is niet gelukt
        catch (IOException e)
        {
            throw new IOException(Message.CLIENT_ERROR_STREAM_INIT);
        }

        String  d           = "\\" + DELIM;
        String  joinRegex   =
                "^" + JOIN + d +                     // commando
                "(" + TRUE + "|" + FALSE + ")" + d + // kan er gechat worden
                "([^" + DELIM + "]+)$";              // de naam van de speler
        Command joinCommand = new JoinCommand(joinRegex);
	
        // gebruikers settings ophalen
        try
        {
        	String command = in.readLine();
        	if (joinCommand.executable(command))
        	{
                joinCommand.run();
        	}
            else
            {
                throw new IOException();
            }
        }
        catch (IOException e)
        {
        	throw new IOException(Message.CLIENT_ERROR_FETCH_USER);
        }
	}
	
	/**
	 * Deze functie wordt aangeroepen na het starten van de Thread.
	 * In een oneindige loop wordt er gecheckt of er nieuwe commando berichten
	 * binnen komen via de socket. Zodra dit het geval is, zal dit bericht 
	 * doorgestuurd worden naar het Game Object. Deze handelt het verder af.
	 */
    @Override
	public void run()
	{
		String s = null;
		try
		{
			while (true) 
			{
				// checken of er nog wat te lezen is.
                s = in.readLine();
				if (s != null)
				{
                    game.executeCommand(this, s);
				}
				else
				{
					throw new IOException();
				}
			}
		}
		// fout, dus we sluiten de boel af
		catch (IOException e)
		{
            this.shutdown();
		}
	}

    /**
     * Met deze methode kan een bericht over de socketverbinding gestuurd
     * worden naar de client. Als dit mislukt kunnen we er vanuit gaan dat
     * de verbinding verbroken is, en sluiten we de boel af.
     * @param msg het bericht dat verstuurd wordt naar de client
     */
    public void sendMessage(String msg)
    {
        // proberen het bericht te versturen
        try
        {
            this.out.write(msg);
            this.out.newLine();
            this.out.flush();
        }
        // er gaat iets fout, dus afsluiten
        catch (IOException e)
        {
            this.game.notifyServer(Message.CLIENT_ERROR_SEND_OUTPUT);
            this.shutdown();
        }
    }
    
    
    /**
     * Deze ClientHandler meldt zich af bij de Server en stuurt
     * vervolgens een laatste broadcast naar de Server om te melden
     * dat de Client niet langer deelneemt aan de chatbox.
     */
    private void shutdown() 
    {
        // Zodra game bestaat, vertellen dat we er mee ophouden. Anders verwijderen
        // we deze clienthandler uit de lijst van de server.
        if (this.game != null)
        {
            this.game.executeCommand(this, QUIT);
        }
        else
        {
            this.server.addMessage(String.format(Message.ELVIS_HAS_LEFT_THE_BUILDING, clientName));
            this.server.removeClientHandler(this);
        }
    	
        // streams en socket afsluiten
    	try
    	{
        	this.in.close();
        	this.out.close();
        	this.sock.close();
        }
        // fout bij het afsluiten
        catch(IOException e)
        {
        	System.out.println(Message.CLIENT_ERROR_CLOSING);
        }
    }
    
	/**
	 * Geeft de naam van de Client terug.
     * @return de naam van de client
	 */
	public String getClientName()
	{
		return this.clientName;
	}
    
    /**
     * Geeft een nieuwe naam aan de client
     * @param name de nieuwe naam
     */
    public void setClientName(String name)
    {
        this.clientName = name;
    }
	
	/**
	 * Geeft aan of de Client, die hoort bij deze ClientHandler, de mogelijkheid 
	 * heeft om te chatten
	 * @return true als de client wel kan chatten, anders false
	 */ 
	public boolean canChat()
	{
		return this.canChat;
	}
	
	/**
	 * Verandert het Game Object waar deze ClientHandler in speelt.
	 * @param g het Game object waar ClientHandler in zit
	 */
	public void setGame(Game g)
	{
		this.game = g;
	}
	
	/**
	 * Klasse die er voor zorgt dat het join commando afgehandeld wordt
	 */
	private class JoinCommand extends Command
	{
		public JoinCommand(String regex)
		{
			super(regex);
		}
		
        /**
         * Handelt het 'join'-commando af
         */
        @Override
        public void run()
		{
			ArrayList<String> params = this.getParameters();
			
			canChat    = new Boolean(params.get(0));
			clientName = params.get(1);
		}
	}
}
